using System;
using System.Collections.Generic;
using System.Drawing;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using ZoneFiveSoftware.Common.Data;
using ZoneFiveSoftware.Common.Data.Measurement;
using ZoneFiveSoftware.Common.Data.Fitness;
using ZoneFiveSoftware.Common.Data.GPS;
using ZoneFiveSoftware.Common.Visuals;
using FilteredStatistics.View;
using FilteredStatistics.Data;

namespace FilteredStatistics.Controller
{
    class Utils
    {
        [DllImport("gdi32.dll")]
        private static extern int BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight,
                                         IntPtr hdcSrc, int nXSrc, int nYSrc, Int32 dwRop);

        [DllImport("gdi32.dll")]
        private static extern IntPtr CreateCompatibleDC(IntPtr hDC);

        [DllImport("gdi32.dll")]
        private static extern bool DeleteDC(IntPtr hdc);

        [DllImport("gdi32.dll")]
        private static extern bool DeleteObject(IntPtr hObject);

        [DllImport("gdi32.dll")]
        private static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);

        static public void RenderBitmapToGraphics(Bitmap source, Graphics destination, Rectangle destinationRect)
        {
            try
            {
                IntPtr destHdc = destination.GetHdc();
                IntPtr bitmapHdc = CreateCompatibleDC(destHdc);
                IntPtr hBitmap = source.GetHbitmap();
                IntPtr oldHdc = SelectObject(bitmapHdc, hBitmap);

                BitBlt(destHdc, destinationRect.Left, destinationRect.Top,
                       destinationRect.Width, destinationRect.Height,
                       bitmapHdc, 0, 0, 0x00CC0020);


                destination.ReleaseHdc(destHdc);
                SelectObject(bitmapHdc, oldHdc);
                DeleteDC(bitmapHdc);
                DeleteObject(hBitmap);
            }
            catch//(System.DllNotFoundException e)
            {
                // Mono/Linux - Just go on right now
                //throw e;
            }
        }

        public static Length.Units MajorLengthUnit(Length.Units unit)
        {
            if(IsMetric(unit))
            {
                return Length.Units.Kilometer;
            }
            else
            {
                return Length.Units.Mile;
            }
        }

        public static Length.Units MinorLengthUnit(Length.Units unit)
        {
            if (IsMetric(unit))
            {
                return Length.Units.Meter;
            }
            else
            {
                return Length.Units.Yard;
            }
        }

        public static Length.Units MinorElevationUnit(Length.Units unit)
        {
            if (IsMetric(unit))
            {
                return Length.Units.Meter;
            }
            else
            {
                return Length.Units.Foot;
            }
        }

        public static bool IsMetric(Length.Units unit)
        {
            return (int)unit <= (int)Length.Units.Kilometer;
        }

        public static bool IsStatute(Length.Units unit)
        {
            return !IsMetric(unit);
        }

        public static string GetMajorSpeedUnitLabelForActivity(IActivity activity)
        {
            string speedUnitLabel = string.Empty;

            if (activity != null)
            {
                speedUnitLabel = Speed.Label(activity.Category.SpeedUnits, new Length(1, MajorLengthUnit(activity.Category.DistanceUnits)));
            }

            return speedUnitLabel;
        }

        public static double SpeedToPace(double speed)
        {
            return Constants.MinutesPerHour / speed;
        }

        public static double PaceToSpeed(double pace)
        {
            return Constants.MinutesPerHour / pace;
        }

        public static bool IsTextIntegerInRange(string text, Int64 minRange, Int64 maxRange)
        {
            Int64 value;

            if (Int64.TryParse(text, out value))
            {
                if (value >= minRange && value <= maxRange)
                {
                    return true;
                }
            }

            return false;
        }

        public static bool IsTextIntegerInRange(string text, Int32 minRange, Int32 maxRange)
        {
            Int32 value;

            if (Int32.TryParse(text, out value))
            {
                if (value >= minRange && value <= maxRange)
                {
                    return true;
                }
            }

            return false;
        }

        public static bool IsTextIntegerInRange(string text, UInt16 minRange, UInt16 maxRange)
        {
            UInt16 value;

            if (UInt16.TryParse(text, out value))
            {
                if (value >= minRange && value <= maxRange)
                {
                    return true;
                }
            }

            return false;
        }

        public static bool IsTextInteger(string text)
        {
            UInt64 value;

            return UInt64.TryParse(text, out value);
        }

        public static bool IsTextFloatInRange(string text, double minRange, double maxRange)
        {
            return IsTextFloatInRange(text, minRange, maxRange, PluginView.UICulture);
        }

        public static bool IsTextFloatInRange(string text, double minRange, double maxRange, CultureInfo culture)
        {
            double value;

            if (double.TryParse(text, NumberStyles.Float, culture.NumberFormat, out value))
            {
                if (value >= minRange && value <= maxRange)
                {
                    return true;
                }
            }

            return false;
        }

        public static bool IsTextTimeInRange(string text, double minRange, double maxRange)
        {
            try
            {
                float value = TimeToFloat(text);

                if (value < minRange || value > maxRange)
                {
                    return false;
                }

                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        public static float TimeToFloat(string time)
        {
            if (time.IndexOf(':') != -1)
            {
                string minutes = time.Substring(0, time.IndexOf(':'));
                string seconds = time.Substring(time.IndexOf(':') + 1);

                float value = 0;

                if (minutes != String.Empty)
                {
                    value += float.Parse(minutes);
                }
                value += float.Parse(seconds) / Constants.SecondsPerMinute;

                return value;
            }
            else
            {
                // We only have minutes
                return float.Parse(time);
            }
        }

        public static IGPSRoute RemovePausedTimesInTrack(IGPSRoute sourceTrack, IActivity activity)
        {
            ActivityInfo activityInfo = ActivityInfoCache.Instance.GetInfo(activity);

            if (activityInfo != null && sourceTrack != null)
            {
                if (activityInfo.NonMovingTimes.Count == 0)
                {
                    return sourceTrack;
                }
                else
                {
                    IGPSRoute result = new GPSRoute();
                    DateTime currentTime = sourceTrack.StartTime;
                    IEnumerator<ITimeValueEntry<IGPSPoint>> sourceEnumerator = sourceTrack.GetEnumerator();
                    IEnumerator<IValueRange<DateTime>> pauseEnumerator = activityInfo.NonMovingTimes.GetEnumerator();
                    double totalPausedTimeToDate = 0;
                    bool sourceEnumeratorIsValid;
                    bool pauseEnumeratorIsValid;

                    pauseEnumeratorIsValid = pauseEnumerator.MoveNext();
                    sourceEnumeratorIsValid = sourceEnumerator.MoveNext();

                    while (sourceEnumeratorIsValid)
                    {
                        bool addCurrentSourceEntry = true;
                        bool advanceCurrentSourceEntry = true;

                        // Loop to handle all pauses up to this current track point
                        if (pauseEnumeratorIsValid)
                        {
                            if (currentTime >= pauseEnumerator.Current.Lower &&
                                currentTime <= pauseEnumerator.Current.Upper)
                            {
                                currentTime = pauseEnumerator.Current.Lower;
                            }
                            else if (currentTime > pauseEnumerator.Current.Upper)
                            {
                                // Advance pause enumerator
                                totalPausedTimeToDate += (pauseEnumerator.Current.Upper - pauseEnumerator.Current.Lower).TotalSeconds;
                                pauseEnumeratorIsValid = pauseEnumerator.MoveNext();

                                // Make sure we retry with the next pause
                                addCurrentSourceEntry = false;
                                advanceCurrentSourceEntry = false;
                            }
                        }

                        if (addCurrentSourceEntry)
                        {
                            result.Add(currentTime.ToLocalTime() - new TimeSpan(0, 0, (int)totalPausedTimeToDate), sourceEnumerator.Current.Value);
                        }

                        if (advanceCurrentSourceEntry)
                        {
                            sourceEnumeratorIsValid = sourceEnumerator.MoveNext();
                            currentTime = sourceTrack.StartTime + new TimeSpan(0, 0, (int)sourceEnumerator.Current.ElapsedSeconds);
                        }
                    }

                    return result;
                }
            }

            return null;
        }

        public static INumericTimeDataSeries RemovePausedTimesInTrack(INumericTimeDataSeries sourceTrack, IActivity activity)
        {
            ActivityInfo activityInfo = ActivityInfoCache.Instance.GetInfo(activity);

            if (activityInfo != null && sourceTrack != null)
            {
                INumericTimeDataSeries result = new NumericTimeDataSeries();

                if (activityInfo.NonMovingTimes.Count == 0)
                {
                    // Remove invalid data nonetheless
                    IEnumerator<ITimeValueEntry<float>> sourceEnumerator = sourceTrack.GetEnumerator();
                    bool sourceEnumeratorIsValid;

                    sourceEnumeratorIsValid = sourceEnumerator.MoveNext();

                    while (sourceEnumeratorIsValid)
                    {
                        if (!float.IsNaN(sourceEnumerator.Current.Value))
                        {
                            result.Add(activityInfo.Activity.StartTime + new TimeSpan(0, 0, (int)sourceEnumerator.Current.ElapsedSeconds), sourceEnumerator.Current.Value);
                        }

                        sourceEnumeratorIsValid = sourceEnumerator.MoveNext();
                    }
                }
                else
                {
                    DateTime currentTime = sourceTrack.StartTime;
                    IEnumerator<ITimeValueEntry<float>> sourceEnumerator = sourceTrack.GetEnumerator();
                    IEnumerator<IValueRange<DateTime>> pauseEnumerator = activityInfo.NonMovingTimes.GetEnumerator();
                    double totalPausedTimeToDate = 0;
                    bool sourceEnumeratorIsValid;
                    bool pauseEnumeratorIsValid;

                    pauseEnumeratorIsValid = pauseEnumerator.MoveNext();
                    sourceEnumeratorIsValid = sourceEnumerator.MoveNext();

                    while (sourceEnumeratorIsValid)
                    {
                        bool addCurrentSourceEntry = true;
                        bool advanceCurrentSourceEntry = true;

                        // Loop to handle all pauses up to this current track point
                        if (pauseEnumeratorIsValid)
                        {
                            if (currentTime > pauseEnumerator.Current.Lower &&
                                currentTime <= pauseEnumerator.Current.Upper)
                            {
                                addCurrentSourceEntry = false;
                            }
                            else if (currentTime > pauseEnumerator.Current.Upper)
                            {
                                // Advance pause enumerator
                                totalPausedTimeToDate += (pauseEnumerator.Current.Upper - pauseEnumerator.Current.Lower).TotalSeconds;
                                pauseEnumeratorIsValid = pauseEnumerator.MoveNext();

                                // Make sure we retry with the next pause
                                addCurrentSourceEntry = false;
                                advanceCurrentSourceEntry = false;
                            }
                        }

                        if (addCurrentSourceEntry && !float.IsNaN(sourceEnumerator.Current.Value))
                        {
                            DateTime entryTime = currentTime - new TimeSpan(0, 0, (int)totalPausedTimeToDate);

                            result.Add(entryTime.ToLocalTime(), sourceEnumerator.Current.Value);
                        }

                        if (advanceCurrentSourceEntry)
                        {
                            sourceEnumeratorIsValid = sourceEnumerator.MoveNext();
                            currentTime = sourceTrack.StartTime + new TimeSpan(0, 0, (int)sourceEnumerator.Current.ElapsedSeconds);
                        }
                    }
                }

                return result;
            }

            return null;
        }

        public static DateTime RemovePausedTimes(DateTime activityTime, IActivity activity)
        {
            if (activity != null)
            {
                ActivityInfo activityInfo = ActivityInfoCache.Instance.GetInfo(activity);

                if (activityInfo != null &&
                    activityInfo.NonMovingTimes.Count > 0)
                {
                    TimeSpan totalPausedTimeToDate = new TimeSpan();

                    foreach (IValueRange<DateTime> pausedRange in activityInfo.NonMovingTimes)
                    {
                        // Pause before our time
                        if (pausedRange.Upper < activityTime)
                        {
                            totalPausedTimeToDate += (pausedRange.Upper - pausedRange.Lower);
                        }
                        else if (activityTime >= pausedRange.Lower &&
                                 activityTime <= pausedRange.Upper)
                        {
                            // Inside a stop, add time difference
                            totalPausedTimeToDate += (activityTime - pausedRange.Lower);
                        }
                    }

                    return activityTime - totalPausedTimeToDate;
                }
                else
                {
                    return activityTime;
                }
            }

            return new DateTime();
        }

        public static TimeSpan GetActiveActivityTime(IActivity activity)
        {
            ActivityInfo activityInfo = ActivityInfoCache.Instance.GetInfo(activity);
            TimeSpan activeTime = activityInfo.ActualTrackTime;

            // The only way to get the active time considering the settings is to use the non moving times
            foreach(IValueRange<DateTime> nonMovingTime in activityInfo.NonMovingTimes)
            {
                activeTime -= nonMovingTime.Upper - nonMovingTime.Lower;
            }

            return activeTime;
        }

        public static void DeserializeTemplatesFromLogbook(ILogbook logbook)
        {
            if (logbook != null)
            {
                byte[] logbookData = logbook.GetExtensionData(GUIDs.PluginMain);
                MemoryStream stream = new MemoryStream(logbookData);

                if (logbookData != null && logbookData.Length > 0)
                {
                    try
                    {
                        SerializableString header = new SerializableString(Constants.DataHeaderIdString);
                        DataVersion version = new DataVersion(0);

                        header.Deserialize(stream, version);

                        if (header == Constants.DataHeaderIdString)
                        {
                            SerializableByteRange versionRead = new SerializableByteRange(0);

                            versionRead.Deserialize(stream, version);

                            if (versionRead <= Constants.CurrentVersion.VersionNumber)
                            {
                                version = new DataVersion(versionRead);
                            }
                            else
                            {
                                MessageBox.Show(PluginView.GetLocalizedString("DataTooRecentErrorText"),
                                                PluginView.GetLocalizedString("ErrorText"),
                                                MessageBoxButtons.OK, MessageBoxIcon.Error);

                                return;
                            }
                        }
                        else if (header == Constants.DataHeaderIdV3String)
                        {
                            SerializableUInt16Range versionRead = new SerializableUInt16Range(0);

                            versionRead.Deserialize(stream, version);

                            if (versionRead <= Constants.CurrentVersion.VersionNumber)
                            {
                                version = new DataVersion(versionRead);
                            }
                            else
                            {
                                MessageBox.Show(PluginView.GetLocalizedString("DataTooRecentErrorText"),
                                                PluginView.GetLocalizedString("ErrorText"),
                                                MessageBoxButtons.OK, MessageBoxIcon.Error);

                                return;
                            }
                        }
                        else
                        {
                            // Corrupted data?  Don't deserialize we'll probably crash
                            return;
                        }

                        TemplateManager.Instance.DeserializeTemplatesFromStream(stream, version);
                    }
                    catch (Exception e)
                    {
                        throw e;
                    }
                }

                stream.Close();
            }
        }

        public static double Cursorize(double value, double min, double max)
        {
            Debug.Assert(min != max);

            double diff = (max - min);

            return (value - min) / diff;
        }

        public static double Clamp(double value, double min, double max)
        {
            return Math.Min(max, Math.Max(value, min));
        }

        public static double CursorizeAndClamp(double value, double min, double max)
        {
            double cursor = Cursorize(value, min, max);

            return Clamp(cursor, 0, 1);
        }

        public static double Lerp(double a, double b, double cursor)
        {
            double diff = (b - a);

            return a + diff * cursor;
        }

        public static IValueRange<DateTime> AddPausesToRange(IValueRange<DateTime> source, IActivity activity)
        {
            if (activity != null)
            {
                ActivityInfo activityInfo = ActivityInfoCache.Instance.GetInfo(activity);

                if(activityInfo.NonMovingTimes.Count == 0)
                {
                    return source;
                }
                else
                {
                    DateTime lowerResultBound = new DateTime(source.Lower.Ticks);
                    DateTime upperResultBound = new DateTime(source.Upper.Ticks);

                    foreach (ValueRange<DateTime> pause in activityInfo.NonMovingTimes)
                    {
                        TimeSpan pauseDuration = pause.Upper - pause.Lower;

                        if (pause.Lower <= lowerResultBound)
                        {
                            // Lower bound after start of pause, add paused time to lower
                            lowerResultBound += pauseDuration;
                        }

                        if (pause.Lower <= upperResultBound)
                        {
                            // Lower bound after start of pause, add paused time to upper
                            upperResultBound += pauseDuration;
                        }
                    }

                    return new ValueRange<DateTime>(lowerResultBound, upperResultBound);
                }
            }

            return null;
        }

        public static IValueRangeSeries<DateTime> AddPausesToRanges(IValueRangeSeries<DateTime> source, IActivity activity)
        {
            if (activity != null)
            {
                ValueRangeSeries<DateTime> result = new ValueRangeSeries<DateTime>();

                foreach (IValueRange<DateTime> currentRange in source)
                {
                    result.Add(AddPausesToRange(currentRange, activity));
                }

                return result;
            }

            return null;
        }

        public static int CompareDateTimeRangeAscending(ValueRange<DateTime> item1, ValueRange<DateTime> item2)
        {
            return (int)(item1.Lower - item2.Lower).TotalSeconds;
        }

        public static bool IsInActivityCategoryHierarchy(IActivity activity, IActivityCategory category)
        {
            IActivityCategory currentCategory = activity.Category;

            do
            {
                if (category == currentCategory)
                {
                    return true;
                }
                else if (!currentCategory.UseParentSettings)
                {
                    return false;
                }

                currentCategory = currentCategory.Parent;
            }
            while (currentCategory != null);

            return false;
        }

        public static double GetStandardDeviation(INumericTimeDataSeries dataSeries)
        {
            double result = 0;

            if (dataSeries != null &&
                dataSeries.Count > 1)
            {
                double average = dataSeries.Avg;
                double sumSquared = 0;

                //Perform the sum of value-average differences squared
                foreach (ITimeValueEntry<float> value in dataSeries)
                {
                    sumSquared += Math.Pow(value.Value - average, 2);
                }

                result = Math.Sqrt(sumSquared / (dataSeries.Count - 1));
            }

            return result;
        }
    }
}
